Skip to content

release: v0.4.11 — stop hijacking the foreground, fix real-device terminate - #190

Merged
justrach merged 2 commits into
mainfrom
release/0.4.11
Jul 25, 2026
Merged

release: v0.4.11 — stop hijacking the foreground, fix real-device terminate#190
justrach merged 2 commits into
mainfrom
release/0.4.11

Conversation

@justrach

Copy link
Copy Markdown
Owner

Why this release exists

kuri could not be run on a machine someone was using. Driving the Simulator seized the foreground window and moved the cursor — on every tap, and every 250ms during a wait-for-ui. That is fixed, along with two real-device bugs that the new tests surfaced.


kuri no longer takes over your machine

Every input command called sim_window.activate first. It had to: CGEventPost(kCGHIDEventTap, …) injects into the global event stream, so events land wherever focus happens to be, and Simulator.app had to be raised for a tap to hit the right thing.

Command Before Now
tap doubletap longpress swipe gesture touch key key-sequence batch global HID tap → had to raise the window and warp the cursor CGEventPostToPid → straight into Simulator's queue
type AppleScript keystroke → goes to the frontmost app Unicode CGEvents addressed to the pid
uitree find wait-for-ui called activate() nothing — AX reads work on a background app
button background called activate() nothing — already used AXPress, which never needed focus
open-sim open -a (raises it) open -g (background)

type was the worst of them: because AppleScript keystroke targets whatever is frontmost, it had to steal focus to be correct — and if it ever ran without doing so, it would have typed your text into whatever you had open. wait-for-ui was a close second, re-stealing the foreground on every poll for the length of the wait.

--activate restores the old behaviour per command, off by default.

Real-device fixes

terminate --device could never have succeeded. It built:

devicectl device process terminate --device <udid> <bundle-id>

but devicectl's terminate takes --pid and accepts no bundle id at all, so every invocation died on devicectl's own argument parser. Now:

$ kuri-mobile ios launch --device --udid <UDID> com.apple.Preferences
pid=40728
$ kuri-mobile ios terminate --device --udid <UDID> --pid 40728     # exit 0
$ kuri-mobile ios terminate --device --udid <UDID> com.apple.Preferences
terminated pid=40731

The bundle-id form resolves a running pid by matching device info processes against the app's on-device bundle URL. A launch that reports success without an identifier is now an error rather than a silent zero — which would otherwise terminate an unrelated process later.

list-apps --device silently hid every system app. devicectl device info apps defaults to developer apps only and nothing in the output says so, so a command documented as "list installed apps" returned a handful of entries on a phone with hundreds, and exited 0. The same defaulting broke bundle-id lookups, so terminate-by-bundle-id could not resolve a system app either.

list-apps on the simulator no longer demands --udid — it resolves the booted simulator like every other simulator command already did.

Diagnosis

"Simulator.app is not running" when it was running. The accessibility tree hangs off a window, and simctl boot does not open one — so a running-but-windowless Simulator produced an error that sent you to restart an app that was already up. Now a distinct SimulatorHasNoWindow error carrying the real remedy, and doctor reports window presence rather than just the process.

Tests

  • zig build e2e-ios-device — new suite against attached hardware. Inspection, the install → list-apps → launch → terminate → uninstall round trip both by pid and by bundle id, and assertions that the XCUITest-only commands still refuse cleanly while a real device is attached. Skips with a reason when nothing is plugged in, no bundle id is set, or the screen is locked — phones re-lock on their own timeout and SpringBoard refuses every launch while they are, which is the environment, not a defect.
  • 21 hermetic real-device contract cases in e2e-ios, needing no hardware. They pin the silent-success class fixed in 0.4.10. Two assert the absence of devicectl's argument-parser complaint — that is what separates "the device is missing" from "we called devicectl wrong", i.e. exactly the terminate bug above.
  • e2e-ios degrades instead of failing on preconditions a machine cannot supply (Accessibility grant, Simulator window, Xcode toolchain), which is what lets it be a CI gate rather than a permanent red build on a runner that can never hold a TCC grant.
  • More simulator coverage: list-apps, status-bar, ui appearance set-and-read-back, set-location/reset-location, log --last, terminate.
  • devicectl's JSON shapes are unit-tested against fixtures — a missing pid must not decode as 0, and /var/Demo.appendix must not match as being inside /var/Demo.app.

Verified

Suite Result Against
e2e-ios-device 24 passed, 0 failed iPhone 16 Pro Max, physically attached
e2e-ios 54 passed, 0 failed booted iPhone 17 Pro simulator
e2e-ios (headless) 44 passed, 0 failed, 1 skipped AX group skips without a Simulator window
zig build test 42 passed

Not verified: the pid-targeted input path. CGEventPostToPid and the Unicode type compile and are the correct APIs for background delivery, but no tap/swipe/type has been run against a simulator since the change — the e2e suites deliberately never post input events, since doing so seizes the cursor of whoever is running them.

CI

The e2e suite finally runs on every push. A new mobile-macos job builds kuri-mobile, runs its unit tests, boots a simulator and runs both suites. Nothing previously caught a regression on the device path — which is how the silent --device no-op survived from 0.4.6 to 0.4.10.

It picks and boots the simulator through kuri-mobile itself rather than xcrun simctl: partly to exercise list-devices and boot for real, and partly because bare xcrun resolves through xcode-select, the exact indirection whose failure mode this project exists to avoid. The job never opens Simulator.app, so it runs headless and the accessibility cases skip.

kuri-mobile's unit tests now also run on the Linux job — it has its own build.zig, so the root test step never reached them.

🤖 Generated with Claude Code

https://claude.ai/code/session_012gzdKWXk7UbHm5TtSGqYBJ

justrach and others added 2 commits July 25, 2026 17:05
…minate

kuri could not be run on a machine someone was using. Every Simulator input
command raised Simulator.app first, because CGEventPost(kCGHIDEventTap) injects
into the global event stream and lands wherever focus happens to be. `type` was
worse: it shelled out to AppleScript `keystroke`, which goes to the frontmost
app, so without stealing focus it would have typed into the user's own window.
And `wait-for-ui` polls every 250ms, re-stealing the foreground each time.

Input now goes to Simulator.app by pid via CGEventPostToPid, `type` uses Unicode
CGEvents, the observation commands do not activate at all, and `open-sim` opens
in the background. `--activate` restores the old behaviour per command.

Two real-device bugs, both found by writing the tests:

- `terminate --device` built `devicectl device process terminate --device <udid>
  <bundle-id>`, but devicectl's terminate takes `--pid` and no bundle id, so it
  could only ever die on devicectl's argument parser. `launch --device` now
  surfaces the pid; `terminate` takes `--pid` or resolves a bundle id itself.
- `list-apps --device` silently hid every system app: devicectl's app listing
  defaults to developer apps only and says nothing about it.

Also: `uitree` reported "Simulator.app is not running" when it was running but
had no window, which sent you to restart an app that was already up.

Verified on hardware — 24 passed / 0 failed against an iPhone 16 Pro Max,
54 passed / 0 failed against a booted simulator, 42 unit tests.

The e2e suite now runs in CI on macOS, which is what would have caught the
0.4.6-to-0.4.10 silent `--device` no-op in the first place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012gzdKWXk7UbHm5TtSGqYBJ
Tool-surface diff by category, what each covers that the other does not, and
how the two test strategies differ.

The two findings worth surfacing: physical-device UI automation is missing from
*both* projects (theirs runs through AXe against the simulator), so kuri's
simulator-only tap/uitree is not a gap against them; and their device "e2e"
tests are mocked against captured argv strings on ubuntu-latest, which cannot
catch either bug fixed in 0.4.11 — a filtered app listing has correct argv, and
a locked device has no argv signature at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012gzdKWXk7UbHm5TtSGqYBJ
@justrach
justrach merged commit 62c0ad0 into main Jul 25, 2026
2 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant